<?php
// $Id
/**
 * DB 数据库操作相关
 *
 * @copyright       Copyright (C) 2010-2012 ARESCMS Inc.
 * @author          TomQin <tomqin@gmail.com>
 * @license         http://www.arescms.cn/license/
 * @package Core
 */


/**
 * Db_MySqli 类
 *
 * @copyright       Copyright (C) 2010-2012 ARESCMS Inc.
 * @author          TomQin <tomqin@gmail.com>
 * @license         http://www.arescms.cn/license/
 * @package Core
 */
class Db_MySqli extends MySqli{
    protected $options = array();
    private $cached_stmt = array();

    public function __construct(array $opts = array()) {
        $this->set_options($opts);

        parent::init(); // init mysqli for options, connection
        mysqli_report(MYSQLI_REPORT_OFF);

        if (!empty($this->options['timeout'])) {
            $ret = parent::options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->options['timeout']);
        }

        //connect database
        $ret = parent::real_connect($this->options['host'], $this->options['username'], $this->options['password'],
                                     $this->options['database'], $this->options['port'], $this->options['sock'],
                                     $this->options['flags']);
        if ($ret === false || $this->connect_errno) {
            $this->db_conn_exception();
        }
        if (!empty($this->options['charset'])) {
            parent::set_charset($this->options['charset']);
        }

        return $ret;
    }

    public function __destruct() {
    }

    protected function db_conn_exception() {
        if ($this->options['throw_exceptions']) {
            throw new Exception($this->connect_error, $this->connect_errno);
        } else {
            echo $this->connect_error.' ['.$this->connect_errno.']<br/>';  //exit();
        }
    }

    protected function db_exception($error='') {
        $errno = $this->errno;
        if ($errno > 0) {
            $error .= ' ['.$this->error.']';
        }
        if ($this->options['throw_exceptions']) {
            throw new Exception($error, $errno);
        } else {
            echo $error.' ['.$errno.']<br/>'; //exit();
        }
    }

    protected function set_options(array $opts) {
        $this->options = array();
        $this->options['server']     = 'prod'; // prod|qa|dev|etc

        $this->options['host']       = '127.0.0.1';
        $this->options['username']   = null;
        $this->options['password']   = null;
        $this->options['database']   = null;

        $this->options['port']       = 3306;
        $this->options['sock']       = null; // socket or named pipe
        $this->options['flags']      = null; // null ignore; MYSQLI_CLIENT_COMPRESS, etc

        $this->options['allow_admin']       = false; // true|false; allow admin() method
        $this->options['throw_exceptions']  = true; // true|false; type of error

        $this->options['charset']    = null; // null ignore, else utf8, etc
        $this->options['timeout']    = null; // null ignore, else nbr secs

        $this->options = array_merge($this->options, $opts);
    }

    /*
     * @param string select sql
     * @param string field types such as 'isdb'
     * @param array
     * @parms int mode
     *      MYSQLI_STORE_RESULT normal;
     *      MYSQLI_USE_RESULT very large results; call $result->close(); before next select()
     * @return Mysqli_Result|false
     */
    public function select($sql, $types=null, $parms = array(), $mode = MYSQLI_STORE_RESULT) {

        $command = $this->get_command($sql);
        if (!in_array($command, array('SELECT', 'EXPLAIN', 'SHOW', 'CALL'))) {
            $this->db_exception('Only SELECT, EXPLAIN, SHOW, CALL allowed via '.__CLASS__.'::select().');
        }

        $result = null;
        if (empty($types) || empty($parms)) {
            $this->real_query($sql);
            if ($this->field_count) {
                $result = ($mode == MYSQLI_USE_RESULT) ? parent::use_result() : parent::store_result();
            }
        } else {
            if (isset($this->cached_stmt[$sql]) && $this->cached_stmt[$sql]) {
                $stmt = $this->cached_stmt[$sql];
            } else {
                $this->cached_stmt[$sql] = $stmt = $this->prepare($sql);
            }

            if ($stmt) {
                $this->stmt_bind_param($stmt, $types, $parms);

                $stmt->execute();
                if ($this->field_count) {
                    $result = $stmt->get_result();
                }
                $stmt->close();
            } else {
                $this->db_exception('Unable to bind parameters.');
            }
        }

        if (!$result && $this->error) {
            $this->db_exception('Unable to select a sql.');
        } else {
            //if ($mode == MYSQLI_USE_RESULT) {
            //  $result->close();
            //}
            return new Db_MySqli_Result($result);
        }
    }

    /*
     * @param string execute sql
     * @param string field types such as 'isdb'
     * @param array
     * @return int|false
     */
    public function execute($sql, $types = null, $parms = array()) {

        $command = $this->get_command($sql);
        if (!in_array($command, array('INSERT', 'UPDATE', 'REPLACE', 'DELETE', 'DROP', 'CREATE', 'GRANT', 'SET'))) {
            $this->db_exception('Only INSERT, UPDATE, REPLACE, DELETE, DROP, CREATE, GRANT, SET allowed via '.__CLASS__.'execute().');
        }

        if (empty($types) || empty($parms)) {
            $this->real_query($sql);
        } else {
            if (isset($this->cached_stmt[$sql]) && $this->cached_stmt[$sql]) {
                $stmt = $this->cached_stmt[$sql];
            } else {
                $this->cached_stmt[$sql] = $stmt = $this->prepare($sql);
            }

            if ($stmt) {
                $this->stmt_bind_param($stmt, $types, $parms);
                $stmt->execute();
                $stmt->close();
            } else {
                $this->db_exception('Unable to bind parameters.');
            }
        }

        if ($this->errno) {
            $this->db_exception('Unable to execute a sql.');
        } else {
            if (in_array($command, array('INSERT'))) {
                $ret = $this->insert_id;
            } else if (in_array($command, array('UPDATE', 'REPLACE', 'DELETE', 'DROP', 'CREATE', 'GRANT', 'SET'))) {
                $ret = $this->affected_rows;
            }
            return $ret;
        }
    }

    /*
     *
     * note: prepare not support mulit query mode (under win32)
     */
    public function proc($sql, $result_type = MYSQLI_ASSOC) {

        $command = $this->get_command($sql);
        if (!in_array($command, array('CALL'))) {
            $this->db_exception('Only CALL allowed via '.__CLASS__.'::proc().');
        }

        $result = $this->multi_query($sql);
        if (!$result && $this->errno) {
            $this->db_exception('Unable to proc a sql.');
        } else if ($result) {
            $data = array();
            do {
                if ($result = $this->store_result()) {
                    $data[] = $result->fetch_all($result_type);
                    $result->free();
                }
            } while ($this->more_results() && $this->next_result());

            return $data;
        } else {
            return ture;
        }
    }

    private function stmt_bind_param(&$stmt, $types, $parms) {
        $params = array();
        foreach($parms as $key => $ref) {
            $params[] = &$parms[$key];
        }
        array_unshift($params, $types);
        $ref = new ReflectionMethod('mysqli_stmt', 'bind_param');
        $ref->invokeArgs($stmt, $params);
    }

    private function get_command($sql) {
        $command = strtoupper(substr($sql, 0, strpos(trim($sql), ' ')));
        return $command;
    }
}

/**
 * Db_MySqli_Result 类
 *
 * @copyright       Copyright (C) 2010-2012 ARESCMS Inc.
 * @author          TomQin <tomqin@gmail.com>
 * @license         http://www.arescms.cn/license/
 * @package Core
 */
class Db_MySqli_Result{
    protected  $result;

    function __construct(Mysqli_Result $result) {
        $this->result = $result;
    }

    public function fetchAll($result_type = MYSQLI_ASSOC) {
        return $this->result->fetch_all($result_type);
    }

    public function fetchArray($result_type = MYSQLI_ASSOC) {
        return $this->result->fetch_array($result_type);
    }

    public function fetchAssoc() {
        return $this->result->fetch_assoc();
    }

    public function fetchCol($index = 0) {
        return $this->result->fetch_row()[$index];
    }

    public function fetchObject($class_name = null, $params = null) {
        if (empty($class_name)) {
            return $this->result->fetch_object();
        } else {
            return $this->result->fetch_object($class_name, $params);
        }
    }

    public function fetchKeyArray($key, $vkey = null) {
        $data = array();
        if ($vkey) {
            while ($row = $this->result->fetch_assoc()) {
                $data[$row[$key]] = $row[$vkey];
            }
        } else {
            while ($row = $this->result->fetch_assoc()) {
                $data[] = $row;
            }
        }
        return $data;
    }

    public function close() {
        $this->result->close();
    }
}

/**
 * Db 类
 *
 * @copyright       Copyright (C) 2010-2012 ARESCMS Inc.
 * @author          TomQin <tomqin@gmail.com>
 * @license         http://www.arescms.cn/license/
 * @package Core
 */
class Db{
    private static $instance;

    public static function getSql($field, $table, $where, $limit = '', $order = '', $group = '') {
        $where = empty($where) ? '' : ' WHERE '.static::where($where);
        $order = empty($order) ? '' : ' ORDER BY '.$order;
        $group = empty($group) ? '' : ' GROUP BY '.$group;
        $limit = empty($limit) ? '' : ' LIMIT '.$limit;

        return 'SELECT '.$field.' FROM '.$table.$where.$group.$order.$limit;
    }

    public static function where($where) {
        if (is_array($where)) {
            $sql = $comm = '';
            foreach($where as $k => $v) {
                $sql .= $comm . "`$k` = " . (is_null($v) ? "NULL" : static::quote($v));
                $comm = ' AND ';
            }
            return $sql;
        } else {
            return $where;
        }
    }

    public static function instance($opts = array()) {
        if (!static::$instance) {
            static::$instance = new Db_MySqli($opts);
        }
        return static::$instance;
    }

    public static function quote($string) {
        return static::instance()->real_escape_string($string);
    }

    public static function select($sql, array $params = array(), $mode = MYSQLI_STORE_RESULT) {
        $size = count($params);
        $stype = ($size > 0) ? str_repeat('s', $size) : '';
        return static::instance()->select($sql, $stype, $params, $mode);
    }

    public static function execute($sql, array $params = array()) {
        $size = count($params);
        $stype = ($size > 0) ? str_repeat('s', $size) : '';
        return static::instance()->execute($sql, $stype, $params);
    }

    public static function proc($sql, $result_type = MYSQLI_ASSOC) {
        return static::instance()->proc($sql, $result_type);
    }
}